<?php
/**
 * scottlib core
 * @author Aaron Collegeman aaroncollegeman.com
 * @version 0.1.3 (alpha): $Id: scottlib.php 160 2009-01-12 13:09:39Z acollegeman $
 *
 * Copyright (C)2008 Collegeman.net, LLC.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 
 */

// ----------------------------------------------------------------------------
// Don't touch anything in this file.  That way when there's an update, you
// can just overwrite this file with the new one.  Go ahead, we warned you!
// ----------------------------------------------------------------------------
@include_once('scottlib-cfg.php');
@include_once('scottlib-orm.php');
@include_once('scottlib-buckets.php');

function array_all_keys_numeric($arr) {
	if (!is_array($arr))
		throw new Exception("Parameter must be an array");
	
	$allKeysAreNumeric = true;
	foreach(array_keys($arr) as $key) {
		if (!is_numeric($key)) {
			$allKeysAreNumeric = false;
			break;
		}	
	}

	return $allKeysAreNumeric;
}
	

/**
 * A wrapper for the resource object returned by mysql_query().  Using this wrapper, 
 * developers can use simple foreach-style looping to extract records from the
 * the underyling result set, e.g.:
 *
 * foreach($results as $record) {
 *     echo $record->field;
 *     ...
 * }
 *
 * By default, the records are extracted using mysql_fetch_object(), but this can
 * be switched at anytime using the methods ::objects(), ::hashes(), and ::arrays().
 * 
 * @since 0.1.0
 */
class slMySqlResultSet implements Iterator {
	
	private $_resource;
	
	private $_position = 0;
	
	private $_active = null;
	
	private $_mode = 'object';
	
	/**
	 * Constructor
	 * @param Object $resource A reference to an object returned by mysql_query()
	 */
	function __construct(/* mysql result set */ $resource) {
		$this->_resource = $resource;
	}
	
	/**
	 * @see Iterator::rewind()
	 */
	function rewind() {
		@mysql_data_seek($this->_resource, 0);
		$this->next();
		$this->_position = 0;
	}
	
	/**
	 * @see Iterator::current9)
	 */
	function current() {
		return $this->_active;
	}
	
	/**
	 * @see Iterator::valid()
	 */
	function valid() {
		return ($this->_active !== null && $this->_active !== false);	
	}
	
	/**
	 * @see Iterator::key()
	 */
	function key() {
		return $this->_position;	
	}
	
	/**
	 * @see Iterator::next()
	 */
	function next() {
		if ($this->_mode == 'object') 
			$this->_active = @mysql_fetch_object($this->_resource);
			
		else if ($this->_mode == 'assoc')
			$this->_active = @mysql_fetch_assoc($this->_resource);
			
		else if ($this->_mode == 'array')
			$this->_active = @mysql_fetch_array($this->_resource);
			
		$this->_position++;
	}
	
	/**
	 * Switches the result set wrapper into object mode, in which records are fetched from the result set
	 * using mysql_fetch_object()
	 * @see mysql_fetch_object()
	 * @since 0.1.0
	 */
	function objects() {
		$this->_mode = 'object';	
		return $this;
	}
	
	/**
 	 * @deprecated Use slMySqlResultSet::hashes() instead.
	 * @since 0.1.0
	 */
	function assocs() {
		return $this->hashes();
	}
	
	/**
	 * Switches the result set wrapper into associative array mode, in which records are fetched from the
	 * result set using mysql_fetch_assoc()
	 * @see mysql_fetch_assoc()
	 * @since 0.1.0
	 */
	function hashes() {
		$this->_mode = 'assoc';	
		return $this;
	}
	
	/**
	 * Switches teh result set wrapper into array mode, in which records are fetched from the result
 	 * set using mysql_fetch_array()
	 * @see mysql_fetch_array()
	 * @since 0.1.0
	 */
	function arrays() {
		$this->_mode = 'array';
		return $this;
	}
	
	/**
	 * @return The number of records held in the underyling result set.
	 */
	function count() {
		return @mysql_num_rows($this->_resource);
	}	
	
	/**
	 * @return The number of fields (i.e., columns) represented in the underlying result set.
	 */
	function fieldCount() {
		return @mysql_num_fields($this->_resource);	
	}
	
	/**
	 * @return The first record stored in the underlying result set, or when the result set is empty, null.
	 */
	function first() {
		if ($this->count() >= 1) {
			$this->rewind();
			$this->next();
			return $this->current();
		}
		else
			return null;
	}
	
}

/**
 * A wrapper for the resource object returned by mysql_connect().
 * 
 * This class, besides providing an object-oriented interface for interacting with
 * connections to MySQL databases, encapsulates a number of methods for preparing and
 * executing queries, and processing the results. 
 *
 * The class also handles the management of multi-level transactions, allowing for
 * database access objects that depend on this class act as if they are unaware
 * of the current state of the transaction, each beginning, committing and rolling-back
 * according to its own internal process.
 *
 * @since 0.1.0
 */
class slMySqlConn {

	private $_host;
	
	private $_username;
	
	private $_password;
	
	private $_database;
	
	private $_conn;
	
	private $_transactionLevel = 0;
	
	private static $_instance;
	
	/**
  	 * When the namespace is available, the function connect() may be
	 * used in place of new slMySqlConn() for creating new connections to the database servers.
	 * @param String $server The host, as a name (domain name), IP address, or socket path
	 * @param String $username The username for connecting to the server
	 * @param String $password The password for connecting to the server
	 * @param String $database The name of the database to use
 	 * @param mixed $client_flags An enumeration of the MySQL constants provided by the MySQL PHP driver, 0 or null.
 	 * @see mysql_connect()
	 * @see http://us2.php.net/manual/en/mysql.constants.php
	 */
	function __construct($server, $username, $password, $database = null, $client_flags = null) {
		ob_start();
		$this->_conn = mysql_connect($server, $username, $password, true, $client_flags);
		$error = ob_get_clean();
		
		if ($this->_conn === false)
			throw new Exception("Failed to connect to server, $username:".preg_replace('/./', '*', $password)."@$server. ".trim($error));
		
		if (!empty($database))
			$this->database($database);
			
		self::$_instance = $this;
	}
	
	/**
 	 * Close the link to the MySQL server.
	 */
	function disconnect() {
		mysql_close($this->_conn);	
	}
	
	/**
	 * @return The most recently instantiated instance of slMySqlConn
	 * @throws Exception When no instance of slMySqlConn has yet been configured.
	 */ 
	static function instance() {
		if (self::$_instance == null)
			throw new Exception("You haven't created an instance of slMySqlConn yet; can't return a value through slMySQLConn::instance until you do.");
		return self::$_instance;
	}
	
	/**
	 * This method provides escaping for arrays. Note that it supports only one dimensional
	 * arrays.  Any value of the array that is itself an array or an object will be
	 * serialized with the standard PHP serialize() function.
	 * @param Array $arr An array whose values should be escaped for insertion into a MySQL query
	 * @return A new array, fully escaped.
	 * @see mysql_esacpe_string()
	 * @see serialize()
	 */
	private static function escapeArray($arr) {
		$values = array();
		foreach ($arr as $k => $v) {
			if (is_numeric($v))
				$values[] = $v;
			else if (is_object($v) || is_array($v))
				$values[] = "'".mysql_escape_string(serialize($v))."'";
			else
				$values[] = "'".mysql_escape_string($v)."'";
		}
		return "(".join(",", $values).")";
	}
	
	/**
	 * A convenience method for inserting a piece of text ($value) into another piece of text ($sql)
	 * at position $offset, replacing another piece of text that begins at $offset and ends
	 * at $offset+$placeholder_length.
	 * @param String $sql The string into which $value should be inserted
	 * @param String $value The string to be inserted
	 * @param int $offset The position in $sql at which $value should be inserted
	 * @param int $placeholder_length The length of the substring in $sql, beginning at $offset, to be replaced by $value
	 */
	private static function punch($sql, $value, $offset, $placeholder_length=1) {
		return substr($sql, 0, $offset).$value.substr($sql, $offset+$placeholder_length);
	}
	
	/**
 	 * Processes various forms of parameterized SQL statements, replacing a variety of placeholders
	 * (some generic and some data-type specific, or on a named basis) with the values specified by $params.
	 *
	 * In addition to value parameters, this method also supports table and column name insertion, which
	 * are delimited with back-ticks instead of apostrophes, as well as pass-through parameters, which
	 * are inserted with neither escaping nor delimiting.
	 *
	 * When the namespace is available, the function prepare() can be used to call this method.
	 *
	 * @param String $sql A parameterized SQL statement
	 * @param Array $params An array of the values to be inserted into $sql
	 * @throws Exception When the number of placeholders in $sql is not equal to the number of values in $params
	 * @throws Exception When a named placeholder does not have a corresponding value in $params
	 */
	static function prepare($sql, $params = array()) {
		if (!is_object($params) && !is_array($params))
			$params = array($params);
		
		// generic parameter mode: 
		// ? (possibly escaped) and # (escaped with backticks) are placeholders, strings are escaped automatically
		// and %d or %n (numeric), %c (date), %s (string) %a (array), and %p (pass-through), escaping dependent upon parameter type
		if (is_array($params) && array_all_keys_numeric($params)) { 
			
			// FIXME: this string ignores escaped placeholders: ([\?\#](in)?|%[nds])
			if (preg_match_all('/([\?\#]|%[dcsapnb])/', $sql, $matches, PREG_OFFSET_CAPTURE)) { 
				
				$placeholders = $matches[0];
				if (($npl = count($placeholders)) != ($npa = count($params)))
					throw new Exception("Number of parameters ($npa) does not equal number of placeholders ($npl)");
				
				for($i=count($placeholders)-1; $i>=0; $i--) {
					
					list($placeholder, $offset) = $placeholders[$i];
					$value = $params[$i];

					// value is null
					if ($value === null)
						$sql = self::punch($sql, 'null', $offset, strlen($placeholder));
					
					// expecting boolean value:
					else if ($placeholder == '%b') {
						if (!is_bool($value) && !is_bool( ($value ? true : false) ))
							throw new Exception("Expecting boolean for parameter $i, found: ".$value);
						$sql = self::punch($sql, ($value ? 1 : 0), $offset, 2);	
					}
						
					// expecting array parameter value:
					else if ($placeholder == '%a') {
						if (!is_array($value))
							throw new Exception("Expecting array for parameter $i, found: ".$value);
						$sql = self::punch($sql, self::escapeArray($value), $offset, 2);
					}
					
					// expecting numeric value:
					else if ($placeholder == '%d' || $placeholder == '%n') {
						if (!is_numeric($value))
							throw new Exception("Expecting numeric for parameter $i, found: ".$value);	
						$sql = self::punch($sql, $value, $offset, 2);
					}
					
					// expecting date value:
					else if ($placeholder == '%c') {
						// timestamp?
						if (is_numeric($value)) {
							if (($date = date('Y/m/d H:i:s', $value)) === false)
								throw new Exception("Failed to create date from timestamp: ".$value);
						}
						
						// strtotime parsable value?
						else { 
							if (($timestamp = strtotime($value)) === false)
								throw new Exception("Failed to parse timestamp from: ".$value);
							$date = date('Y/m/d H:i:s', $timestamp);
						}
						
						$sql = self::punch($sql, "'".$date."'", $offset, 2);
					}
					
					// expecting string value:
					else if ($placeholder == '%s') {
						if (!is_string($value) && !is_numeric($value))
							throw new Exception("Expecting string for parameter $i, found: ".$value);
						$escaped = "'".mysql_escape_string($value)."'";
						$sql = self::punch($sql, $escaped, $offset, 2); 	
					}
					
					// punch-through and numeric parameter values:
					else if ($placeholder == '%p') {
						$sql = self::punch($sql, $value, $offset, 2);	
					}
					
					// no escaping: 
					else if ($placeholder == '#') { 
						$sql = self::punch($sql, "`".$value."`", $offset);
					}
					
					// numeric parameter value:
					else if (is_numeric($value)) {
						$sql = self::punch($sql, $value, $offset);	
					}
					
					// escaping:
					else { 
						$escaped = "'".mysql_escape_string($value)."'";
						$sql = self::punch($sql, $escaped, $offset);
					}
				}
			}
		}
		
		// named parameter mode:
		// parameter denoted by colon (e.g., ":name" is parametered named "name")
		// escaping dependent upon parameter value and placeholder prefix: ":" results in normal escaping, "#" in system escaping with backticks
		else { 
			// FIXME: object property parsing
			
			if (preg_match_all('/(\!)?([\:\#])([\w\_]+)\b/i', $sql, $matches, PREG_OFFSET_CAPTURE)) {
				list($fulltext, $optionals, $types, $placeholders) = $matches;  
				
				for($i=count($placeholders)-1; $i>=0; $i--) {
					list($type) = $types[$i];
					$optional = $optionals[$i];
					list ($placeholder, $offset) = $placeholders[$i];	
					
					// standard named parameter:
					if ($type == ':') {
						
						// optional named parameter: use null in place of missing parameter values
						if (!isset($params[$placeholder])) {
							if ($optional)
								$sql = self::punch($sql, 'null', $offset-2, strlen($placeholder)+2);
							else
								throw new Exception("Parameter value not provided for placeholder [$placeholder].");
						}
						
						// required parameters:
						else {
							$value = $params[$placeholder];
							if (is_numeric($value))
								$sql = self::punch($sql, $value, $offset-1, strlen($placeholder)+1);
							else {
								$sql = self::punch($sql, "'".mysql_escape_string($value)."'", $offset-1, strlen($placeholder)+1);
							}
						}
					}
					
					else if ($type == '#') {
						if (!isset($params[$placeholder]))
							throw new Exception("Parameter value not provided for placeholder [$placeholder].");
						
						$value = $params[$placeholder];
						$sql = self::punch($sql, "`".$value."`", $offset-1, strlen($placeholder)+1);
					}
				}
			}
		}
		
		return $sql;
	}
	
	/**
	 * Selects database $name on the database server to which this object is connected.
	 * @param String $name The name of the database
	 * @return slMySqlConn A reference to this connection object
	 * @throws Exception When attempt to connect to database $name fails
	 * @see mysql_select_db()
	 */ 
	function database($name) {
		if (!@mysql_select_db($name, $this->_conn))
			throw new Exception("Failed to connect to database $name: ".mysql_error($this->_conn));	
		return $this;
	}
	
	/**
	 * Executes $sql for the purpose of retrieving a result set.
	 *
	 * When the namespace is available, the function query() can be used to call this method. 
	 *
	 * @param String $sql The query to be executed
	 * @return slMySqlresultSet wrapped around a MySQL result set resource object
	 * @throws Exception When execution of $sql fails
	 */
	function query($sql) {
		if (($resource = @mysql_query($sql, $this->_conn)) === false)
			throw new Exception("Failed to execute query: ".mysql_error($this->_conn));
		return new slMySqlResultSet($resource);
	}
	
	/**
	 * Executes $sql for the purpose of retrieving a result set.
	 * This method is an alias for slMySqlConn::query.
	 * When the namespace is available, the function select() can be used to call this method.
 	 * @see slMySqlConn::query
	 */
	function select($sql) {
		return $this->query($sql);	
	}
	
	/**
	 * Executes $sql for the purpose of retrieving the value of the first
	 * field of the first record returned by the query.
     *
	 * When the namespace is available, the function val() can be used to call this method.
	 *
	 * @param String $sql The query to be executed
	 * @param mixed $default A value to be returned when the value queried from the database is null
	 * @return Either the value loaded by $sql or $default
	 * @throws Exeception When the execution of $sql fails
	 */
	function val($sql, $default = null) {
		$r = @mysql_query($sql, $this->_conn);
		if ($r === false)
			throw new Exception('Failed to execute query: '.mysql_error($this->_conn));
		else {
			if ($rs = @mysql_fetch_array($r)) {
				$value = $rs[0];
				return ($value == null ? $default : $value);
			}
			else
				return $default;	
		}
	}
	
	/**
	 * Executes $sql for the purpose of changing data stored in the database.
	 * @param String $sql The statement to be executed
	 * @returns int The number of records affected by the query
	 * @throws Exception When execution of the statement fails
	 */
	function execute($sql) {
		if (!@mysql_query($sql, $this->_conn))
			throw new Exception("Failed to execute query: ".mysql_error($this->_conn));
		return mysql_affected_rows($this->_conn);	
	}
	
	/**
	 * This method is an alias for the method slMySqlConn::execute
	 * When the namespace is available, the function update() may be used to call this method.
	 * @see slMySqlConn::execute
	 */
	function update($sql) {
		return $this->execute($sql);	
	}
	
	/**
	 * This method is an alias for the method slMySqlConn::insert
	 * When the namespace is available, the function insert() may be used to call this method.
	 * @see slMySqlCon::insert
	 */
	function insert($sql) {
		return $this->execute($sql);	
	}
	
	/**
	 * This method is an alias for the method slMySqlConn::execute
	 * When the namespace is available, the function delete() may be used to call this method.
	 * @see slMySqlConn::delete
	 */
	function delete($sql) {
		return $this->execute($sql);
	}	
	
	/**
	 * This method increments the nested transaction level.  If prior to incrementing the nested
	 * level is 0, then the MySQL instance is instructed to disable autocommitting and begin
	 * a new transaction.
	 *
	 * When the namespace is available, the function beginTransaction() may be used to call this method.
	 */
	function begin() {
		if ($this->_transactionLevel === 0) {
			$this->execute('SET AUTOCOMMIT=0');
			$this->execute('BEGIN');	
		}
		$this->_transactionLevel++;
	}
	
	/**
	 * This method decrements the nested transaction level.  After decrementing, if the nested
	 * level is 0, then the MySQL instance is instructed to commit the current transaction and
	 * enable autocommitting.
	 *
	 * When the namespace is available, the function commit() may be used to call this method.
	 */
	function commit() {
		$this->_transactionLevel--;
		if ($this->_transactionLevel === 0) {
			$this->execute('COMMIT');
			$this->execute('SET AUTOCOMMIT=1');
		}
	}
	
	/**
	 * This method requests a rollback of the current transaction, if any.
	 *
	 * When the namespace is available, the function rollback() may be used to call this method.
	 */
	function rollback() {
		if ($this->_transactionLevel > 0) {
			$this->_transactionLevel = 0;
			$this->execute('ROLLBACK');
			$this->execute('SET AUTOCOMMIT=1');
		}
	}
} /* end slMySqlConn */

/**
 * scottlib automatically detects the Symfony Framework, and, if defined, uses
 * the application configuration group "scottlib" (properties server, username,
 * password, and database) to instantiate an instance of slMySqlConn.
 */
if (class_exists('sfConfig')) {
	global $sl_db;
	if ($config = sfConfig::get('app_scottlib')) {
		$sl_db = new slMySqlConn(
			$config['server'], 
			$config['username'], 
			$config['password'], 
			$config['database'], 
			eval($config['client_flags'])
		);	
	}
}

if (!function_exists('beginTransaction')) {
	/**
	 * @see slMySqlConn::begin()
	 */
	function beginTransaction(slMySqlConn &$conn = null) {
		$instance = ($conn == null) ? slMySqlConn::instance() : $conn;
		$instance->begin();
	}
}

if (!function_exists('begin')) {
	/**
	 * @see slMySqlConn::begin()
	 */
	function begin(slMySqlConn &$conn = null) {
		$instance = ($conn == null) ? slMySqlConn::instance() : $conn;
		$instance->begin();
	}
}

if (!function_exists('commit')) {
	/**
	 * @see slMySqlConn::commit()
	 */
	function commit(slMySqlConn &$conn = null) {
		$instance = ($conn == null) ? slMySqlConn::instance() : $conn;
		$instance->commit();
	}
}

if (!function_exists('rollback')) {
	/**
	 * @see slMySqlConn::rollback()
	 */
	function rollback(slMySqlConn &$conn = null) {
		$instance = ($conn == null) ? slMySqlConn::instance() : $conn;
		$instance->rollback();
	}
}

if (!function_exists('connect')) {
	/**
	 * @see slMySqlConn::connect()
	 */
	function connect($server, $username, $password, $database = null, $client_flags = null) {
		return new slMySQLConn($server, $username, $password, $database, $client_flags);
	}
}

if (!function_exists('disconnect')) {
	/**
  	 * @see slMySqlConn::disconnect()
	 */
	function disconnect(slMySqlConn $conn = null) {
		($conn == null ? db()->disconnect() : $conn->disconnect());
	}
}

if (!function_exists('db')) {
	/**
	 * @see slMySqlConn::instance()
	 */
	function db() {
		return slMySQLConn::instance();
	}
}

if (!function_exists('prepare')) {
	/**
	 * @see slMySqlConn::prepare()
	 */
	function prepare($sql, $params = array()) {
		return slMySqlConn::prepare($sql, $params);
	}
}

if (!function_exists('val')) {
	/**
	 * @see slMySqlConn::val()
	 */
	function val($sql, $default = null, slMySqlConn &$conn = null) {
		$instance = ($conn == null) ? slMySqlConn::instance() : $conn;
		return $instance->val($sql, $default);
	}
}

if (!function_exists('query')) {
	/**
	 * @see slMySqlConn::query()
	 */
	function query($sql, slMySqlConn &$conn = null) {
		$instance = ($conn == null) ? slMySqlConn::instance() : $conn;
		return $instance->query($sql);
	}
}

if (!function_exists('execute')) {
	/**
	 * @see slMySqlConn::execute()
	 */
	function execute($sql, slMySqlConn &$conn = null) {
		$instance = ($conn == null) ? slMySqlConn::instance() : $conn;
		return $instance->execute($sql);
	}
}

if (!function_exists('update')) {
	/**
	 * @see slMySqlConn::update()
	 */
	function update($sql, slMySqlConn &$conn = null) {
		$instance = ($conn == null) ? slMySqlConn::instance() : $conn;
		return $instance->update($sql);
	}
}

if (!function_exists('select')) {
	/**
	 * @see slMySqlConn::select()
	 */
	function select($sql, slMySqlConn &$conn = null) {
		$instance = ($conn == null) ? slMySqlConn::instance() : $conn;
		return $instance->select($sql);
	}
}

if (!function_exists('insert')) {
	/**
	 * @see slMySqlConn::insert()
	 */
	function insert($sql, slMySqlConn &$conn = null) {
		$instance = ($conn == null) ? slMySqlConn::instance() : $conn;
		return $instance->insert($sql);
	}
}

if (!function_exists('delete')) {
	/**
	 * @see slMySqlConn::delete()
	 */
	function delete($sql, slMySqlConn &$conn = null) {
		$instance = ($conn == null) ? slMySqlConn::instance() : $conn;
		return $instance->delete($sql);
	}
}


/**
 * A wrapper for an array of objects, allowing for the wrapped array to be treated like
 * the array or like the object stored in the first index of the first dimension of the array.
 * 
 * @since 0.1.3
 */
class slObjectArrayWrapper implements Iterator, ArrayAccess {
	
	private $_arr;
	
	private $_i = 0;
	
	function __construct($arr) {
		$this->_arr = $arr;
	}
	
	function current() {
		return (isset($this->_arr[$this->_i]) ? $this->_arr[$this->_i] : null);
	}
	
	function key() {
		return $this->_i;
	}
	
	function next() {
		$this->_i++;
	}
	
	function rewind() {
		$this->_i = 0;
	}
	
	function valid() {
		return (isset($this->_arr[$this->_i]));
	}
	
	function offsetExists($offset) {
		return isset($this->_arr[$offset]);	
	}
	
	function offsetGet($offset) {
		return $this->_arr[$offset];	
	}
	
	function offsetSet($offset, $value) {
		$this->_arr[$offset] = $value;	
	}
	
	function offsetUnset($offset) {
		unset($this->_arr[$offset]);	
	}
	
	function count() {
		return count($this->_arr);	
	}
	
	function __get($name) {
		return (isset($this->_arr[0])) ? $this->_arr[0]->$name : null;
	}
	
	function __set($name, $value) {
		if (isset($this->_arr[0])) {
			$obj = $this->_arr[0];
			$obj->$name = $value; 
		}
	}
	
	function __call($name, $args) {
		if (isset($this->_arr[0]))
			return call_user_method($name, $this->_arr[0], $args);	
		else
			return null;
	}
	
}

/**
 * Utility class for caching and extracting annotation data from classes and objects.
 * @since 0.1.3
 */
class ReflectionUtils {

	static $_annotations = array();
	
	final static function &getCachedReflection($class_or_object) {
		$className = (is_string($class_or_object) ? $class_or_object : get_class($class_or_object));
		if (!$className)
			throw new Exception("Failed to load class for $class_or_object");
			
		if (!array_key_exists($className, self::$_annotations)) {
			$class = new ReflectionClass($className);
			$cache = array(
				'class' => 	$class,
				'class_a' => self::getAnnotations($class),
				'properties' => $class->getProperties(),
				'properties_a' => false,
				'methods' => $class->getMethods(),
				'methods_a' => false
			);
			
			// echo '<pre>getCachedReflection: '.print_r($cache, true).'</pre>';
			
			self::$_annotations[$className] = &$cache;
		}
		
		return self::$_annotations[$className];
	}
	
	final static function getAnnotations(/* mixed */ $object) {
		if (in_array(get_class($object), array('ReflectionClass', 'ReflectionMethod', 'ReflectionProperty')))
			$doc = $object->getDocComment();
		else {
			$class = new ReflectionClass(get_class($object));
			$doc = $class->getDocComment();	
		}
		
		$annotations = array();
		if (preg_match_all('/@(\w+) ?(\((.*)\))?/', $doc, $matches)) {
			$names = $matches[1];
			$values = $matches[3];
			foreach($names as $i => $name)
				$annotations[$name] = trim($values[$i]);
		}
		
		return $annotations;
	}
	
	final static function getClassAnnotation($class_or_object, $annotation) {
		$cache = self::getCachedReflection($class_or_object);
		return (array_key_exists($annotation, $cache['class_a']) ? $cache['class_a'][$annotation] : false);
	}
	
	final static function hasClassAnnotation($class_or_object, $annotation) {
		if (!is_array($annotation))
			$annotation = array($annotation);
		
		foreach($annotation as $a) {
			if (self::getClassAnnotation($class_or_object, $a) !== false)
				return true;
		}
		
		return false;
	}
	
	final static function getMethodAnnotation($class_or_object, $methodName, $annotation) {
		$cache = &self::getCachedReflection($class_or_object);
		if ($cache['methods_a'] === false) {
			$annotations = array();
			foreach($cache['methods'] as $method)
				$annotations[$method->getName()] = self::getAnnotations($method);
			$cache['methods_a'] = $annotations;	
		}
		
		if (!array_key_exists($methodName, $cache['methods_a']))
			throw new Exception("Method $methodName does not exist");
		
		return (array_key_exists($annotation, $cache['methods_a'][$methodName]) ? $cache['methods_a'][$methodName][$annotation] : false);
	}
	
	final static function hasMethodAnnotation($class_or_object, $methodName, $annotation) {
		if (!is_array($annotation))
			$annotation = array($annotation);
		
		foreach($annotation as $a) {
			if (self::getMethodAnnotation($class_or_object, $propertyName, $a) !== false)
				return true;
		}
		
		return false;
	}
	
	final static function getPropertyAnnotation($class_or_object, $propertyName, $annotation) {
		$cache = &self::getCachedReflection($class_or_object);
		
		if (!is_array($cache['properties_a'])) {
			$annotations = array();
			foreach($cache['properties'] as $property) 
				$annotations[$property->getName()] = self::getAnnotations($property);
				
			$cache['properties_a'] = $annotations;	
		}
		
		if (!array_key_exists($propertyName, $cache['properties_a']))
			throw new Exception("Property $propertyName does not exist");
		
		return (array_key_exists($annotation, $cache['properties_a'][$propertyName]) ? $cache['properties_a'][$propertyName][$annotation] : false);
	}
	
	final static function hasPropertyAnnotation($class_or_object, $propertyName, $annotation) {
		if (!is_array($annotation))
			$annotation = array($annotation);
		
		foreach($annotation as $a) {
			if (self::getPropertyAnnotation($class_or_object, $propertyName, $a) !== false)
				return true;
		}
			
		return false;
	}
}


// command line hook
if (isset($argv) && count($argv) && !class_exists('UnitTestCase'))
	require_once("scottlib-cmd.php");	